1<script setup lang="ts">
2const config = useRuntimeConfig().public;
3const route = useRoute();
4
5const { data: post } = await useAsyncData(`post-${route.path}`, () =>
6 queryCollection("posts").path(route.path).first()
7);
8
9useSeoMeta({
10 title: post.value?.title,
11 description: post.value?.description
12});
13
14if (post.value) {
15 defineOgImageComponent("Post", {
16 title: post.value.title,
17 description: post.value.description,
18 date: post.value.date,
19 author: post.value.authors[0]?.name
20 });
21}
22</script>
23
24<template>
25 <div
26 class="grow"
27 >
28 <NuxtLink
29 class="print:hidden flex items-center gap-1 text-sm text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 mt-4"
30 to="/"
31 >
32 <Icon name="ri:arrow-drop-left-line" mode="svg" />
33 Go back
34 </NuxtLink>
35 <article
36 v-if="post"
37 class="pb-24"
38 >
39 <header class="mb-8 mt-4">
40 <h1 class="text-4xl font-bold">
41 {{ post.title }}
42 </h1>
43 <div class="flex flex-wrap items-center justify-start gap-4 mt-4 text-neutral-600 dark:text-neutral-300">
44 <div v-if="post.authors" class="flex items-center gap-1">
45 <img v-if="post.authors[0]?.name === config.author" src="/logo.png" :alt="post.authors[0].name"
46 class="w-8 h-8 rounded-full mr-2">
47 <span v-for="author in post.authors" :key="author.name">
48 {{ author.name }}
49 </span>
50 </div>
51 ·
52 <span>
53 {{ new Date(post.date).toLocaleDateString('en-GB', {
54 year: 'numeric',
55 month: 'long',
56 day: 'numeric'
57 }) }}
58 </span>
59 <span v-if="post.updated">
60 <span class="text-neutral-500 dark:text-neutral-400 ml-2 mr-4">·</span>
61 <span class="text-sm">
62 Updated: {{ new Date(post.updated).toLocaleDateString('en-GB', {
63 year: 'numeric',
64 month: 'long',
65 day: 'numeric'
66 }) }}
67 </span>
68 </span>
69 ·
70 <div>
71 <span v-for="tag in post.tags" :key="tag" class="mr-2 mb-2 px-3 py-1 text-xs md:text-sm bg-stone-200 dark:bg-stone-700 text-stone-600 dark:text-stone-400 rounded-full">
72 {{ tag }}
73 </span>
74 </div>
75 </div>
76
77 <div class="bg-stone-200 dark:bg-stone-700 h-[1px] my-4"></div>
78
79 <p
80 class="text-md text-neutral-500 dark:text-neutral-400 leading-7 my-8 text-justify md:w-[80%] mx-auto"
81 >
82 {{ post.description }}
83 </p>
84 </header>
85
86 <div class="bg-stone-200 dark:bg-stone-700 h-[1px] my-8 md:w-[80%] mx-auto"></div>
87
88 <TableOfContents v-if="config.tableOfContents" :post="post" />
89
90 <ContentRenderer :value="post" class="post-body prose prose-lg leading-7 prose-slate dark:prose-invert text-justify md:w-[80%] mx-auto text-zinc-800 dark:text-zinc-200" />
91
92 <ShareActions :title="post.title" :description="post.description" :author="post.authors[0]?.name" />
93
94 <Suspense>
95 <BskyComments v-if="post.bskyCid" :cid="post.bskyCid" />
96
97 <template #fallback>
98 <h1 class="md:w-[80%] mx-auto mt-16 text-xl font-bold text-stone-600">Loading comments...</h1>
99 </template>
100 </Suspense>
101
102 </article>
103
104 <div v-else class="flex items-center justify-center">
105 <div class="text-center">
106 <h1 class="text-4xl font-bold">404</h1>
107 <p class="text-neutral-500">Page not found</p>
108 </div>
109 </div>
110 </div>
111</template>
112
113<style>
114.post-body p:first-of-type::first-line {
115 font-weight: bold;
116}
117
118:target:before {
119 content: "";
120 display: block;
121 height: 2rem;
122 margin: -2rem 0 0;
123}
124
125</style>